/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.cloud.oracle.adm;

import com.oracle.bmc.adm.ApplicationDependencyManagementClient;
import com.oracle.bmc.adm.model.KnowledgeBase;
import com.oracle.bmc.adm.requests.GetKnowledgeBaseRequest;
import com.oracle.bmc.adm.responses.GetKnowledgeBaseResponse;
import com.oracle.bmc.model.BmcException;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.cloud.oracle.OCIManager;
import org.netbeans.modules.cloud.oracle.OCIProfile;
import org.netbeans.modules.cloud.oracle.OCISessionInitiator;
import org.netbeans.modules.cloud.oracle.items.OCID;
import org.netbeans.spi.project.AuxiliaryProperties;
import org.netbeans.spi.project.LookupProvider.Registration.ProjectType;
import org.netbeans.spi.project.ProjectServiceProvider;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;

/**
 * This is a pseudo API that allows an integration LSP module to 
 * configure and work with the vulnerability feature. Allows to temporarily configure a knowledge
 * base for a given project. The setting is kept just in memory.
 * 
 * @author sdedic
 */
@ProjectServiceProvider(service = ProjectVulnerability.class, projectTypes = {
    @ProjectType(id = "org-netbeans-modules-maven"),
    @ProjectType(id = "org-netbeans-modules-gradle")
})
public final class ProjectVulnerability {
    private final RequestProcessor AUDIT_PROCESSOR = new RequestProcessor(ProjectVulnerability.class.getName(), 4);
    private final RequestProcessor CALL_PROCESSOR = new RequestProcessor(ProjectVulnerability.class);
    private static final String PROJECT_PROPERTY_PROFILE_ID = "cloud.oracle.adm.kb.profilePath"; // NOI18N
    private static final String PROJECT_PROPERTY_PROFILE_PATH = "cloud.oracle.adm.kb.profileId"; // NOI18N
    private static final String PROJECT_PROPERTY_KB_OCID = "cloud.oracle.adm.kb.ocid"; // NOI18N
    
    private final Project project;
    
    // @GuardedBy(this)
    private KnowledgeBaseItem knowledgeBaseItem = null;
    
    // @GuardedBy(this)
    private OCIProfile lastProfile;

    public ProjectVulnerability(Project project) {
        this.project = project;
    }
    
    public KnowledgeBaseItem getProjectKnowledgeBase() {
        synchronized (this) {
            if (knowledgeBaseItem != null) {
                return knowledgeBaseItem;
            }
        }
        AuxiliaryProperties props = project.getLookup().lookup(AuxiliaryProperties.class);
        if (props == null) {
            return null;
        }
        OCIProfile active = OCIManager.getDefault().getActiveProfile();
        
        String ocid = props.get(PROJECT_PROPERTY_KB_OCID, false);
        if (ocid == null) {
            return null;
        }
        String profId = props.get(PROJECT_PROPERTY_PROFILE_ID, false);
        String profPath = props.get(PROJECT_PROPERTY_PROFILE_PATH, false);
        
        KnowledgeBaseItem item;
        OCIProfile profile;
        
        try {
            profile = OCIManager.forConfig(Paths.get(profPath), profId);
            if (!profile.getTenancy().isPresent()) {
                profile = active;
            }
        } catch (IllegalArgumentException ex) {
            profile = active;
        }
        try (ApplicationDependencyManagementClient admClient
                = profile.newClient(ApplicationDependencyManagementClient.class)) {
            GetKnowledgeBaseRequest req = GetKnowledgeBaseRequest.builder()
                    .knowledgeBaseId(ocid).build();
            GetKnowledgeBaseResponse resp = admClient.getKnowledgeBase(req);
            KnowledgeBase p = resp.getKnowledgeBase();

            String tenancyId = active.getTenancy().isPresent() ?
                    active.getTenancy().get().getKey().getValue() : null;
            String regionCode = active.getRegion().getRegionCode();

            item = new KnowledgeBaseItem(
                    OCID.of(p.getId(), "KnowledgeBase"), // NOI18N 
                    p.getCompartmentId(),
                    p.getDisplayName(), p.getTimeUpdated(),
                    tenancyId,
                    regionCode
                );
        } catch (IllegalArgumentException | BmcException ex) {
            item = knowledgeBaseItem;
            profile = this.lastProfile;
        }
        synchronized (this) {
            if (knowledgeBaseItem == null) {
                lastProfile = profile;
                knowledgeBaseItem = item;
            }
        }
        return item;
    }
    
    @NbBundle.Messages({
        "# {0} - project name",
        "MSG_CreatingAuditFailed=Creating Vulnerablity audit for project {0} failed.",
    })
    public CompletableFuture<AuditResult> runProjectAudit(KnowledgeBaseItem item, AuditOptions options) {
        if (item != null) {
            // make transient setting, so that change handler may work against the same knowledgebase.
            setProjectKnowledgeBase0(item);
        }
        CompletableFuture<AuditResult> result = new CompletableFuture<>();
        AUDIT_PROCESSOR.post(() -> {
            try {
                result.complete(VulnerabilityWorker.getInstance().vulnerabilityAudit(project, options));
            } catch (ThreadDeath x) {
                throw x;
            } catch (AuditException ex) {
                final String projectDisplayName = ProjectUtils.getInformation(project).getDisplayName();
                if (!options.isSupressErrors()) {
                    ErrorUtils.processError(ex, Bundle.MSG_CreatingAuditFailed(projectDisplayName));
                }
                result.completeExceptionally(ex);
            } catch (Exception | Error e) {
                result.completeExceptionally(e);
            }
        });
        // PENDING: handle CF.cancel() by cancelling the WorkRequest
        return result;
    }
    
    public CompletableFuture<KnowledgeBaseItem> findKnowledgeBase(String knowledgeBaseId) {
        CompletableFuture<KnowledgeBaseItem> result = new CompletableFuture<>();
        CALL_PROCESSOR.post(() -> {
            try ( ApplicationDependencyManagementClient client 
                    = OCIManager.getDefault().getActiveSession().newClient(ApplicationDependencyManagementClient.class)) {

                GetKnowledgeBaseRequest request = GetKnowledgeBaseRequest.builder()
                        .knowledgeBaseId(knowledgeBaseId).
                        build();

                OCISessionInitiator session = OCIManager.getDefault().getActiveProfile();
                String tenancyId = session.getTenancy().isPresent() ?
                        session.getTenancy().get().getKey().getValue() : null;
                String regionCode = session.getRegion().getRegionCode();

                GetKnowledgeBaseResponse response = client.getKnowledgeBase(request);
                KnowledgeBase p = response.getKnowledgeBase();
                result.complete(new KnowledgeBaseItemProxy(
                        OCID.of(p.getId(), "KnowledgeBase"), // NOI18N 
                        p.getCompartmentId(),
                        p.getDisplayName(), p.getTimeUpdated(),
                        tenancyId,
                        regionCode)
                );
            } catch (ThreadDeath x) {
                throw x;
            } catch (BmcException ex) {
                result.completeExceptionally(new AuditException(ex.getStatusCode(), ex.getOpcRequestId(), ex.getMessage(), ex));
            } catch (Exception | Error e) {
                result.completeExceptionally(e);
            }
        });
        return result;
    }
    
    
    /**
     * This special item just fires a refresh in any "real" KnowledgeBaseItems that may have been created
     * + presented in the IDE UI.
     */
    static class KnowledgeBaseItemProxy extends KnowledgeBaseItem {
        public KnowledgeBaseItemProxy(OCID id, String compartmenId, String displayName, Date timeUpdated, String tenancyId, String regionCode) {
            super(id, compartmenId, displayName, timeUpdated, tenancyId, regionCode);
        }

        @Override
        void registerItem() {
            // do not register self.
        }

        @Override
        public void refresh() {
            super.refresh();
            Collection<KnowledgeBaseItem> delegates = findKnownInstances(getKey());
            for (KnowledgeBaseItem kbi : delegates) {
                kbi.refresh();
            }
        }
    }
    
    public KnowledgeBaseItem setProjectKnowledgeBase(KnowledgeBaseItem item) {
        setProjectKnowledgeBase0(item);
        AuxiliaryProperties props = project.getLookup().lookup(AuxiliaryProperties.class);
        if (props != null) {
            OCID kbOcid = item.getKey();
            OCIProfile profile = OCIManager.getDefault().getActiveProfile(item);
            props.put(PROJECT_PROPERTY_KB_OCID, kbOcid.toPersistentForm(), false);
            props.put(PROJECT_PROPERTY_PROFILE_ID, profile.getId(), false);
            props.put(PROJECT_PROPERTY_PROFILE_PATH, profile.getConfigPath().toString(), false);
        }
        return item;
    }
    
    private KnowledgeBaseItem setProjectKnowledgeBase0(KnowledgeBaseItem item) {
        synchronized (this) {
            lastProfile = OCIManager.getDefault().getActiveProfile(item);
            return knowledgeBaseItem = item;
        }
    }
}
